package de.e2it3t.schulverwaltung.data;

import java.lang.reflect.Field;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Helfer Klasse für den Umgang mit JDBC. <br>
 * <br>
 * Im Grunde sieht die Verwendung von JDBC immer Gleich aus:<br>
 * <br>
 * &bull; wenn wir Daten aus einer Datenbank brauchen, senden wir einen SQL
 * Select, iterieren über das Result Set und packen die Daten in Objekte<br>
 * <br>
 * &bull; wenn wir Daten in die Datenbank einfügen oder bestehende Daten
 * verändern wollen, entnehmen wir alle Daten dem Objekt, verpacken deise in
 * einen SQL Insert oder Update und senden diesen an die Datenbank<br>
 * <br>
 * Es ist immer der selbe mühselige und redudante Prozess. Es gibt nahezu immer
 * eine Klasse im Code, welche eine Tabelle in der Datenbank representiert.
 * Dieser Helfer nimmt uns diese Aufgabe ab. Er transformiert Daten von Objekten
 * zu SQL und umgekehrt und ermöglicht einen komfortablen Zugriff auf die
 * Datenbank.<br>
 * <br>
 * <b>Was muss ich tun?</b><br>
 * Man muss die Klassen erstellen, welche die Tabellen repräsentieren. Die
 * Klasse muss mit {@link JdbcEntity} markiert sein. Weiterhin enthält die
 * Klasse private Member, welche die typisiert die einzelnen Spalten der Tabelle
 * repräsentieren. Jeder Member der dies tut, muss mit {@link JdbcColumn}
 * markiert sein und damit den Namen der Spalte angeben. Damit ist die Klasse
 * fertig als Kontainer für Daten einer Tabelle verwendet zu werden. Welche
 * Tabelle das sein wird ist unerheblich, die Typen der Attribute und die
 * Deklarierten Spaltennamen müssen aber zur Tabelle passen. Zu beachten ist,
 * dass die Klasse unbedingt einen Default-Konstruktor besitzen muss!<br>
 * <br>
 * <b>Wie verwende ich den JDBC Helfer?</b><br>
 * Man legt den Helfer mit einer bestehenden JDBC Verbindung an. Auf dieser
 * werden die SQL Queries ausgeführt. Man nennt dem Query über die Setter die
 * Bedingungen und Parameter der nächsten Datenbankanfrage. Anschließend führt
 * man eine geeignete Methode aus um das Ergebnis zu bekommen. Danach werden
 * alle Bedingungen und Parameter gelöscht und das Ganze beginnt von vorne. Mehr
 * erfährst du in de Dokumentation der einzelnen Methoden.<br>
 * <br>
 * <b>Was tun bei Fehleren?</b><br>
 * Dieser Helfer wird unweigerlich eine RuntimeException werfen, wenn der
 * Mapping-Vorgang fehlschlägt. Irren ist aber menschlich. Entweder vergisst man
 * die Klasse zu markieren, vergisst Attribute für Spalten anzulegen oder gibt
 * eine flasche Klasse für eine Tabelle an. In all diesen Fällen kann man dem
 * Stacktrace den wahren Grund für den Fehlschlag entnehmen. Die oberste 
 * Exception steht nur für den Fehlschlag selbst. Aber "weiter unten" kann man
 * die Ursache (Exception), die zum Fehlschlag geführt hat, finden.<br>
 * <br>
 * <b>Wie funktioniert das?</b><br>
 * Diser Helfer verwendet ein Konzept, welches sich Reflection nennt. Dieses
 * ermöglicht zur Laufzeit informationen über Klassen zu ermtteln und auf
 * deren Attribute dynamisch zuzugreifen. Aus einem SQL Select Result Set
 * erfahren wir, welche Spaltennamen zurückgegeben wurden. Somit sind wir in der
 * Lage beliebige (in diesem Fall unsere explizit mit Annotations
 * gekennzeichneten) Klassen mit den Daten zu füllen.
 * 
 * @author Viktor Reiser
 */
public class JdbcHelper {
    
    private Connection connection;
    
    private boolean checkColumns;
    private String rawQuery;
    private Object[] parameters;
    private String tableName;
    private String where;
    private String[] selectColumns;
    private String[] columnsToIgnore;
    private String primaryColumnDefault = "id";
    private Object neutralPrimaryValueDafault = 0;
    private String primaryColumn;
    private Object neutralPrimaryValue;
    
    
    /**
     * Helfer mit einer Datenbankverbindung erstellen.
     * 
     * @param connection Datenbankverbindung
     */
    public JdbcHelper(Connection connection) {
        this.connection = connection;
        resetAfterQuery();
    }
    
    
    /**
     * Standartwerte für Primary Key Informationen. <br>
     * <br>
     * Diese Werte werden automatisch nach jedem Request wieder hergestellt.
     * 
     * @param columnName Spaltenname der Primary Key Spalte
     * @param neutralValue Wert, der eine noch nicht gesetzte ID repräsentiert
     * 
     * @see #setPrimary(java.lang.String, java.lang.Object) 
     */
    public void setPrimaryDefaults(String columnName, Object neutralValue) {
        primaryColumnDefault = columnName;
        neutralPrimaryValueDafault = neutralValue;
    }
    
    /**
     * Primary Key Informationen für den nächsten Reuqest setzten. <br>
     * <br>
     * Diese Informationen werden beim Speichern oder Löschen von Daten
     * verwendet, um zu erkennen, ob ein Datensatz neu ist und um einen
     * Datensatz eindeutig in der Datenbank zu identifizieren.
     * 
     * @param columnName Spaltenname der Primary Key Spalte
     * @param neutralValue Wert, der eine noch nicht gesetzte ID repräsentiert
     * 
     * @return Helfer, zum Verketten von Methodenaufrufen
     * 
     * @see #setPrimaryDefaults(java.lang.String, java.lang.Object) 
     */
    public JdbcHelper setPrimary(String columnName, Object neutralValue) {
        primaryColumn = columnName.trim();
        neutralPrimaryValue = neutralValue;
        
        return this;
    }
    
    /**
     * Rohen query beim nächsten Request ausführen. <br>
     * <br>
     * Manchma möchte man die komplette Kontrolle über den Query haben. Man muss
     * dann selbst darauf achten, dass man die richtigen Spalten mit dem
     * richtigen Namen selektiert.
     * 
     * @param rawQuery roher SQL Select Query
     * 
     * @return Helfer, zum Verketten von Methodenaufrufen
     * 
     * @see #setParameters(java.lang.Object[]) 
     */
    public JdbcHelper setRawQuery(String rawQuery) {
        tableName = null;
        selectColumns = new String[0];
        
        this.rawQuery = rawQuery;
        
        return this;
    }
    
    /**
     * Setze den Tabellenname, der für die nächste Anfrage verwendet wird.
     * 
     * @param tableName Tabellenname in der Datenbank
     * 
     * @return Helfer, zum Verketten von Methodenaufrufen
     * 
     * @see #setWhere(java.lang.String)
     * @see #setSelectColumns(java.lang.String)
     */
    public JdbcHelper setTableName(String tableName) {
        rawQuery = null;
        
        this.tableName = tableName.trim();
        
        return this;
    }
    
    /**
     * Setze Parameter für die nächste Anfrage. <br>
     * <br>
     * Man kann in einem JDBC SQL Query Parameter (Fragezeichen) definieren und
     * diese typsicher im Query ergänzen lassen. Die Anzahl der Parameter muss
     * mit der Anzahl der Fragezeichen übereinstimmen.
     * 
     * @param parameters beliebige Pramter, die im nächsten Query ergänzt werden
     * 
     * @return Helfer, zum Verketten von Methodenaufrufen
     */
    public JdbcHelper setParameters(Object ... parameters) {
        checkTableName();
        
        this.parameters = parameters;
        
        return this;
    }
    
    /**
     * Setze die rohe WHERE Bedingugn für den nächsten Request. <br>
     * <br>
     * z.B. "name = ?"
     * 
     * @param where SQL WHERE Bedingung
     * 
     * @return Helfer, zum Verketten von Methodenaufrufen
     * 
     * @see #setParameters(java.lang.Object[]) 
     */
    public JdbcHelper setWhere(String where) {
        checkTableName();
        
        this.where = where.trim();
        
        return this;
    }
    
    /**
     * Zu selektierende Spalten für den nächsten Request. <br>
     * <br>
     * Wenn nicht gesetzt wurde wird "*" genommen (nur wenn
     * {@link #setColumnsToIgnore(java.lang.String)} nicht gesetzt ist).
     * 
     * @param selectColumns die zu selektierenden Spalten
     * 
     * @return Helfer, zum Verketten von Methodenaufrufen
     */
    public JdbcHelper setSelectColumns(String ... selectColumns) {
        checkTableName();
        
        this.selectColumns = selectColumns;
        
        return this;
    }
    
    /**
     * Zu ignorierende Spaltennamen für den nächsten Request festlegen. <br>
     * <br>
     * Dies hat ja nach Anfrgeart verschiedene Bedeutungen:<br>
     * <br>
     * &bull; werden Daten per {@link #setTableName(java.lang.String)}
     * angefragt und {@link #setSelectColumns(java.lang.String)} ist nicht
     * gesetzt, dann wird aus dem JDBC Datan Objekt die möglichen Spaltennamen
     * ermitteln und die hier gegeben davon ausgenommen<br>
     * <br>
     * &bull; werden Daten in die Datenbank eingefügt, dann werden die hier
     * gegebenen Spaltennamen ignorert (ncht eingefügt und nicht geupdatet)
     * 
     * @param columnsToIgnore zu ignorierende Spalten
     * 
     * @return Helfer, zum Verketten von Methodenaufrufen 
     */
    public JdbcHelper setColumnsToIgnore(String ... columnsToIgnore) {
        this.columnsToIgnore = columnsToIgnore;
        
        return this;
    }
    
    /**
     * 
     * @param checkColumns
     * 
     * @return Helfer, zum Verketten von Methodenaufrufen
     */
    public JdbcHelper setCheckColumns(boolean checkColumns) {
        this.checkColumns = checkColumns;
        
        return this;
    }
    
    
    /**
     * Wie viele Datensätze sind vohanden? <br>
     * <br>
     * {@link #setTableName(java.lang.String)} Pflicht<br>
     * {@link #setWhere(java.lang.String)} Optional<br>
     * {@link #setParameters(java.lang.Object[])} Optional
     * 
     * @return Anzahl der Datensaätze in de Datenbank
     */
    public int countResults() {
        checkTableName();
        
        String query = "SELECT COUNT(*) FROM " + tableName + getWhere();
        
        try {
            ResultSet result = executeSQL(query).getResultSet();
            
            resetAfterQuery();
            
            result.next();
            
            return result.getInt(1);
        } catch (SQLException ex) {
            // sollte nicht passieren, da wir den Query selbst konstruiert habe
            // invalider Tabellenname oder Parameter scheitern schon vorher
            throw new RuntimeException(ex);
        }
    }
    
    /**
     * Gibt es Datensätze? <br>
     * <br>
     * {@link  #countResults()} != 0
     * 
     * @return {@code true} wenn Datensätze in der Datenbank vorhanden sind
     */
    public boolean hasResults() {
        return countResults() != 0;
    }
    
    /**
     * Füge Daten in die Datenbank ein oder update bestehende Daten. <br>
     * <br>
     * {@link #setTableName(java.lang.String)} Pflicht<br>
     * {@link #setColumnsToIgnore(java.lang.String)} Optional<br>
     * 
     * @param jdbcObject ein Objekt einer mit Annotations gemappten Klasse,
     * welche die Daten enthält
     * 
     * @see #setPrimary(java.lang.String, java.lang.Object) 
     * @see #setPrimaryDefaults(java.lang.String, java.lang.Object) 
     */
    public void save(Object jdbcObject) {
        checkTableName();
        
        Class<?> jdbcClass = jdbcObject.getClass();
        
        checkObjectClass(jdbcClass);
        
        Map<String, Field> fields = getColumnFieldMap(jdbcClass, true);
        Set<String> columnSet = fields.keySet();
        String query = null;
        
        try {
            boolean isUpdate = false;
            
            if (fields.containsKey(primaryColumn)) {
                Field primaryField = fields.get(primaryColumn);
                Object value = primaryField.get(jdbcObject);
                
                if (neutralPrimaryValue == null) {
                    isUpdate = value != null;
                } else {
                    isUpdate = !neutralPrimaryValue.equals(value);
                }
            }
            
            columnsToIgnore = Arrays.copyOf(
                    columnsToIgnore, columnsToIgnore.length + 1);
            columnsToIgnore[columnsToIgnore.length - 1] = primaryColumn;
            
            query = isUpdate ? createUpdate(columnSet) : createInsert(columnSet);
            
            PreparedStatement stmt = connection.prepareStatement(query);
            
            int i = 1;
            
            for (String column : columnSet) {
                if (!ignoreColumn(column)) {
                    stmt.setObject(i++, fields.get(column).get(jdbcObject));
                }
            }
            
            if (isUpdate) {
                stmt.setObject(i, fields.get(primaryColumn).get(jdbcObject));
            }
            
            stmt.executeUpdate();
            
            resetAfterQuery();
        } catch (Exception ex) {
            throw new RuntimeException(
                    "Failed to save data to database!"
                    + (query != null ? "Query: " + query : ""), ex);
        }
    }
    
    /**
     * Lösche Datensätze aus der Datenbank. <br
     * <br>
     * {@link #setTableName(java.lang.String)} Pflicht<br>
     * {@link #setWhere(java.lang.String)} Optional<br>
     * {@link #setParameters(java.lang.Object[])} Optional<br>
     * <br>
     * Wenn keine Bedingungen gesetzt sind, dann wird die Tabelle komplett
     * geleert!
     */
    public void delete() {
        checkTableName();
        
        executeSQL("DELETE FROM " + tableName + getWhere());
        resetAfterQuery();
    }
    
    /**
     * Lösche einen Datensatz anhand des Primary Keys. <br>
     * <br>
     * {@link #setTableName(java.lang.String)} Pflicht<br>
     * 
     * @param primaryKey Wert des Primary Keys des Datensatzes
     * 
     * @see #setPrimary(java.lang.String, java.lang.Object) 
     * @see #setPrimaryDefaults(java.lang.String, java.lang.Object) 
     */
    public void delete(Object primaryValue) {
        checkTableName();
        
        parameters = new Object[] {primaryValue};
        executeSQL("DELETE FROM " + tableName + " WHERE " + primaryColumn + "=?");
        resetAfterQuery();
    }
    
    /**
     * Erhalte Datensätze anhand der gegebenen Kriterien. <br>
     * <br>
     * {@link #setRawQuery(java.lang.String)} Pflicht<br>
     * oder<br>
     * {@link #setTableName(java.lang.String)} Pflicht<br>
     * {@link #setSelectColumns(java.lang.String)} Optional<br>
     * {@link #setColumnsToIgnore(java.lang.String)} Optional<br>
     * <br>
     * {@link #setCheckColumns(boolean)} Optional
     * 
     * @param <T> Klasse, auf die die Daten gemappt werden 
     * @param jdbcClass Klasse, auf die die Daten gemappt werden
     * 
     * @return 
     * 
     * @see #setPrimary(java.lang.String, java.lang.Object) 
     * @see #setPrimaryDefaults(java.lang.String, java.lang.Object) 
     */
    public <T> List<T> getList(Class<T> jdbcClass) {
        StringBuilder query = new StringBuilder();;
        
        if (rawQuery != null) {
            query.append(rawQuery);
        } else  if (tableName != null) {
            query.append("SELECT ");
            
            if (selectColumns.length != 0) {
                for (int i = 0; i < selectColumns.length; i++) {
                    if (i != 0) {
                        query.append(",");
                    }
                    
                    query.append(selectColumns[i]);
                }
            } else if (columnsToIgnore.length == 0) {
                query.append("*");
            } else {
                for (String column : getColumnFieldMap(jdbcClass, false).keySet()) {
                    if (!ignoreColumn(column)) {
                        query.append(column);
                        query.append(",");
                    }
                }
                
                query.delete(query.length() - 1, query.length());
            }
            
            query.append(" FROM ");
            query.append(tableName);
        } else {
            throw new RuntimeException("No raw query or table name given!");
        }
        
        query.append(getWhere());
        
        try {
            ResultSet result = executeSQL(query.toString()).getResultSet();
            List<T> list = mapResultSet(result, jdbcClass);
            resetAfterQuery();
            
            return list;
        } catch (Exception ex) {
            throw new RuntimeException(
                    "Failed to execute or map SQL query: " + query, ex);
        }
    }
    
    /**
     * Erhalte einen Datensatz anhand des Primary Keys. <br>
     * <br>
     * {@link #setTableName(java.lang.String)} Pflicht<br>
     * {@link #setSelectColumns(java.lang.String)} Optional<br>
     * {@link #setColumnsToIgnore(java.lang.String)} Optional<br>
     * <br>
     * {@link #setCheckColumns(boolean)} Optional
     * 
     * @param <T> Klasse, auf die die Daten gemappt werden 
     * @param primaryValue Wert des Primary Keys des Datensatzes
     * @param jdbcClass Klasse, auf die die Daten gemappt werden
     * 
     * @return gemappter Datensatz oder {@code null}, wenn kein Datensatz mit
     * mit dem gegebenen Primary Key Wert existiert
     * 
     * @see #setPrimary(java.lang.String, java.lang.Object) 
     * @see #setPrimaryDefaults(java.lang.String, java.lang.Object) 
     */
    public <T> T get(Object primaryValue, Class<T> jdbcClass) {
        rawQuery = null;
        where = primaryColumn + "=?";
        parameters = new Object[] {primaryValue};
        
        List<T> list = getList(jdbcClass);
        
        return list.size() > 0 ? list.get(0) : null;
    }
    
    
    // Private JDBC Helfermethoden =============================================
    
    private <T> List<T> mapResultSet(ResultSet result, Class<T> jdbcClass) {
        try {
            checkObjectClass(jdbcClass);
            
            String [] columnNames = getColumnNamesFromResult(result);
            Map<String, Field> columnFields = getColumnFieldMap(jdbcClass, false);
            checkColumns(columnNames, columnFields, jdbcClass);
            
            List<T> objects = new ArrayList<T>();
            
            while (result.next()) {
                objects.add(getObjectFromResultSet(
                        result, jdbcClass, columnNames, columnFields));
            }
        
            return objects;
        } catch (Exception ex) {
            throw new RuntimeException(
                    "Failed to map JDBC result set to object!", ex);
        }
    }
    
    private void checkObjectClass(Class<?> jdbcClass) {
        // wenn die Klasse nicht markiert ist, lehnen wir ab diese mit Daten
        // zu befüllen
        if (!jdbcClass.isAnnotationPresent(JdbcEntity.class)) {
            throw new RuntimeException("Given class (" + jdbcClass.getName()
                    + ") is not a annotated with JdbcEntity!");
        }
    }
    
    private void checkColumns(String [] columnNames,
            Map<String, Field> columnFields, Class<?> jdbcClass) {
        if (checkColumns) {
            // stelle sicher, dass alle Attribute vorhanden sind
            // somit vermeiden wird, dass alle Daten gesetzt werden können, die
            // im JDBC Ergebnis vorhanden sind
            for (int i = 0; i < columnNames.length; i++) {
                if (columnFields.get(columnNames[i]) == null) {
                    throw new RuntimeException("Result set has a "
                            + columnNames[i] + " column but "
                            + jdbcClass.getName()
                            + " doesn't define a field for that column!");
                }
            }
        }
    }
    
    private String[] getColumnNamesFromResult(ResultSet result) throws SQLException {
        // extrahiere alle Spalten die im JDBC Ergebnis vorhanden sind
        ResultSetMetaData meta = result.getMetaData();
        String [] names = new String [meta.getColumnCount()];
        
        for (int i = 0; i < names.length; i++) {
            names[i] = meta.getColumnName(i + 1).toLowerCase();
        }
        
        return names;
    }
    
    private Map<String, Field> getColumnFieldMap(Class<?> jdbcClass, boolean ignorePersist) {
        // extrahiere alle Attribute der Klasse, welche für das Mapping markiert
        // wurden: Spaltenname -> Reflektiertes Feld für späteren Zugriff
        Map<String, Field> fields = new HashMap<String, Field>();
        
        for (Field field : jdbcClass.getDeclaredFields()) {
            // dieses Attribut ist nicht für das Mapping markiert, überspringen
            if (!field.isAnnotationPresent(JdbcColumn.class)) {
                continue;
            }
            
            JdbcColumn column = field.getAnnotation(JdbcColumn.class);
            
            if (ignorePersist && column.ignorePersist()) {
                continue;
            }
            
            // leere Spaltennamen sind nicht erlaubt
            if (column.name() == null || column.name().trim().equals("")) {
                throw new RuntimeException( jdbcClass.getName()
                        + " defines an null or empty column value on "
                        + field.getName() + "!");
            }
            
            field.setAccessible(true);
            fields.put(column.name().trim().toLowerCase(), field);
        }
        
        if (jdbcClass.getSuperclass().isAnnotationPresent(JdbcEntity.class)) {
            Map<String, Field> superFields = getColumnFieldMap(
                    jdbcClass.getSuperclass(), ignorePersist);
            
            for (String fieldName : superFields.keySet()) {
                if (!fields.containsKey(fieldName)) {
                    fields.put(fieldName, superFields.get(fieldName));
                }
            }
        }
        
        return fields;
    }
    
    private static <T> T getObjectFromResultSet(ResultSet result,
            Class<T> jdbcClass, String [] columnNames,
            Map<String,Field> columnFields) throws SQLException {
        T object;
        
        try {
            // erstelle Datenobjekt per Reflection
            // auf diesem Objekt setzen wir gleich die Attribute
            object = (T) jdbcClass.newInstance();
        } catch (Exception ex) {
            throw new RuntimeException(jdbcClass.getName()
                    + " has no default constructor!", ex);
        }
        
        // iteriere über alle Spalten des JDBC Ergenisses und setze die
        // Attribute auf dem Objekt
        for (int i = 0; i < columnNames.length; i++) {
            Object value = result.getObject(i + 1);
            String column = columnNames[i];
            Field field = columnFields.get(column);
            
            // Spaltenwerte vorhanden, aber Attribut nicht, überspringen
            // dies würde nicht passieren, wenn wir beim Aufruf das Flag gesetzt
            // haben, dass wir sicherstellen wollen, dass alle notwendigen
            // Attribute für Spalten im JDBC Ergebnis vorhanden sein sollen
            if (field == null) {
                continue;
            }

            try {
                // setzte den Spaltenwert auf dem Objekt
                field.set(object, value);
            } catch (Exception ex) {
                // dies kann fehlschlagen, wenn man z.B. den falschen Datentyp
                // für das Attribut gesetzt hat (z.B. Attribut ist ein int, aber
                // der Wert aus der Datenbank ein String)
                throw new RuntimeException("Failed to set " + column + " in "
                        + jdbcClass.getName() + " from result set!", ex);
            }
        }
        
        return object;
    }
    
    
    private void resetAfterQuery() {
        checkColumns = false;
        columnsToIgnore = new String[0];
        selectColumns = new String[0];
        rawQuery = null;
        tableName = null;
        parameters = new Object[0];
        where = null;
        primaryColumn = primaryColumnDefault;
        neutralPrimaryValue = neutralPrimaryValueDafault;
    }
    
    private void checkTableName() {
        if (tableName == null) {
            throw new RuntimeException("Table name has to be set before!");
        }
    }
    
    private String getWhere() {
        return where == null || where.equals("") ? "" : " WHERE " + where;
    }
    
    private PreparedStatement executeSQL(String query) {
        PreparedStatement stmt;
        
        try {
            stmt = connection.prepareStatement(query);
        } catch (SQLException ex) {
            throw new RuntimeException(
                    "Failed to create SQL statement from query: " + query, ex);
        }
            
        try {
            int i = 1;

            for (Object parameter : parameters) {
                stmt.setObject(i++, parameter);
            }
        } catch (SQLException ex) {
            throw new RuntimeException(
                    "Failed to bind " + parameters.length
                    + " parameters for SQL statement from query: " + query, ex);
        }
        
        try {
            stmt.execute();
        } catch (SQLException ex) {
            throw new RuntimeException(
                    "Failed to execute SQL statement from query: " + query, ex);
        }
        
        return stmt;
    }
    
    private boolean ignoreColumn(String column) {
        for (String columnToIgnore : columnsToIgnore) {
            if (column.equals(columnToIgnore)) {
                return true;
            }
        }
        
        return false;
    }

    private String createInsert(Set<String> columnSet) {
        StringBuilder sb = new StringBuilder();
        sb.append("INSERT INTO ");
        sb.append(tableName);
        sb.append(" (");
        
        for (String column : columnSet) {
            if (!ignoreColumn(column)) {
                sb.append(column);
                sb.append(",");
            }
        }
        
        sb.delete(sb.length() - 1, sb.length());
        sb.append(") VALUES (");
        
        for (int i = 0; i < columnSet.size() - columnsToIgnore.length; i++) {
            sb.append("?,");
        }
        
        sb.delete(sb.length() - 1, sb.length());
        sb.append(");");
        
        return sb.toString();
    }
    
    private String createUpdate(Set<String> columnSet) {
        StringBuilder sb = new StringBuilder();
        sb.append("UPDATE ");
        sb.append(tableName);
        sb.append(" SET ");
        
        for (String column : columnSet) {
            if (!ignoreColumn(column)) {
                sb.append(column);
                sb.append("=?,");
            }
        }
        
        sb.delete(sb.length() - 1, sb.length());
        sb.append(" WHERE id=?");
        
        return sb.toString();
    }
}
